Skip to content

feat(http): opt-in filter for HTTP endpoint discovery (CustomizeHttpEndpointDiscovery) - #1

Draft
uniquelau wants to merge 1 commit into
mainfrom
feat/customize-http-endpoint-discovery
Draft

feat(http): opt-in filter for HTTP endpoint discovery (CustomizeHttpEndpointDiscovery)#1
uniquelau wants to merge 1 commit into
mainfrom
feat/customize-http-endpoint-discovery

Conversation

@uniquelau

@uniquelau uniquelau commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Review copy on our fork — please review before I open this against JasperFx/wolverine. Natural neighbour to the merged JasperFx#3373 / the live JasperFxGH-3371 (fix/api-description-provider-http-graph), which touches the same HttpGraph discovery area.

Problem

Message handlers can be split across hosts by namespace — Discovery.CustomizeHandlerDiscovery(q => q.Excludes.InNamespace(...)) — but HTTP endpoints cannot.

An …Endpoint/…Endpoints type (or any [WolverineHttpMethod] method) in an excluded namespace of a scanned assembly still registers its routes. [WolverineIgnore] per endpoint type is the only lever, and it doesn't scale to a module/namespace split.

This is the missing half of the modular-monolith-deployed-as-multiple-hosts story Wolverine already supports for handlers (MultipleHandlerBehavior.Separated).

Change (additive, opt-in, default unchanged)

New method on WolverineHttpOptions, a byte-for-byte mirror of CustomizeHandlerDiscovery:

app.MapWolverineEndpoints(opts =>
    opts.CustomizeHttpEndpointDiscovery(q => q.Excludes.InNamespace("MyApp.Excluded")));
  • WolverineHttpOptions.CustomizeHttpEndpointDiscovery(Action<JasperFx.Core.TypeScanning.TypeQuery>) — lazily creates an internal TypeQuery and hands the caller its Includes/Excludes.
  • HttpGraph.DiscoverEndpoints passes it to HttpChainSource; the discovery predicate layers the user query onto the built-in convention (Excludes subtractive, Includes additive). When unset, the predicate reduces to the exact prior condition — default discovery is byte-for-byte unchanged.
  • Delegate is Action<TypeQuery> (the public JasperFx type the handler API already exposes), not Action<CompositeFilter<Type>> — mirrors CustomizeHandlerDiscovery exactly and avoids leaking Wolverine.Http's own internal CompositeFilter<T>.

Test

http_endpoint_discovery_filter (Wolverine.Http.Tests): proves the excluded-namespace endpoint is discovered by default and absent under the filter, while its sibling still resolves. Passes 2/2; regression suites scanning the same test assembly (generate_openapi_without_database, openapi_shape_tests) pass 17/0. Wolverine.Http.csproj builds clean.

Open points for your call before upstream

  1. API name / delegate type — Action<TypeQuery> for exact handler-discovery symmetry (agreed?).
  2. Keep additive-Includes support, or trim to excludes-only for minimalism (the strict ask was excludes-only).
  3. Static/AOT mode inherits the filter transitively via the pre-generated registry (correct — worth stating so it's not read as a gap).

Value: closes a documented asymmetry (the HTTP endpoints.md guide can gain the same filter example handlers/discovery.md already shows), and benefits every multi-host-from-one-build split (public vs admin host, plugin assemblies), not just our use.

Message handlers can be split across hosts via
Discovery.CustomizeHandlerDiscovery(q => q.Excludes.InNamespace(...)), but HTTP
endpoints could not: HttpGraph.DiscoverEndpoints built an HttpChainSource from the
assembly list alone, and HttpChainSource's TypeQuery never consulted any
user-supplied filter, so an endpoint in an excluded namespace of a scanned assembly
still registered. [WolverineIgnore] on the type was the only lever.

Add WolverineHttpOptions.CustomizeHttpEndpointDiscovery(Action<TypeQuery>), the HTTP
counterpart to HandlerDiscovery.CustomizeHandlerDiscovery. HttpChainSource layers the
supplied TypeQuery on top of the built-in endpoint convention: Excludes are
subtractive (drop endpoint types, e.g. q.Excludes.InNamespace(...)), Includes are
additive (broaden discovery; an included type still only contributes methods carrying
a Wolverine HTTP verb attribute). When the method is never called the discovery
predicate is byte-for-byte identical to prior behaviour.

Test proves both directions against a scanned assembly: by default an endpoint in the
excluded namespace is discovered; with the filter it is not, while its sibling
namespace still resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@uniquelau
uniquelau force-pushed the feat/customize-http-endpoint-discovery branch from fe69a74 to c081709 Compare July 20, 2026 08:49
uniquelau pushed a commit that referenced this pull request Aug 1, 2026
Profiled (full-suite TRX, local M-series): 60 classes, 931s of test time,
largest class 167s — a 5.6x parallelism ceiling, so the suite halves even at
the 2-worker clamp on hosted runners.

What the shakeout run at 4 workers surfaced, all fixed here:

- Servers.SqlServerDatabaseName joins PostgresDatabaseName: the catalog
  parsed from the effective connection string, for assertions that were
  hard-coding 'master'.
- LaneDatabases.Name scopes the sibling databases some suites create beside
  the main catalog (multi-tenancy's db1/db2/db3, the NServiceBus
  dedicated-database names) to the lane's catalog, so lanes cannot collide
  on them. Against the default master catalog the names are unchanged.
- static_multi_tenancy's store-description and durability-agent-URI
  assertions now build from the configured names (bug shape #1 from the
  Marten lane work — nine tests there, three here).
- saga_storage_operations.concurrency_exception_when_version_does_not_match
  opened with a raw 'delete from lightweight_sagas...' that bypassed the
  schema API's ensure-storage — on any fresh catalog it failed with
  'Invalid object name' unless a sibling test had auto-created the table
  first. Test-ordering luck; the cleanup was unnecessary (fresh Guid) and
  is gone.

Also: CISqlServer launches the container before compiling and gates on
readiness after (LaunchDockerServices/AwaitDockerServices split), so the
boot rides the compile instead of following it.

Verified: supervised run at 4 workers, 390/390, zero retries, 5m37s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013eR4GL278688VhyhrGcttJ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant